﻿namespace NavigateTo2013
{
    using Microsoft.VisualStudio.Language.NavigateTo.Interfaces;
    using System;
    using System.Collections.Generic;
    using System.Data.OleDb;
    using System.IO;
    using System.Linq;

    public static class SearchEngine
    {
        public static readonly string More = "?more";

        static readonly string defaultPathFilter = @"SCOPE = 'file:'";
        static readonly string defaultFileExtensionFilter =
                  @"System.FileExtension IS NOT NULL AND
                  System.FileExtension != '.obj' AND
                  System.FileExtension != '.pdb' AND
                  System.FileExtension != '.ilk' AND
                  System.FileExtension != '.cache' AND
                  System.FileExtension != '.suo' AND
                  System.FileExtension != '.sln' AND
                  System.FileExtension != '.filters' AND
                  System.FileExtension != '.pch' AND
                  System.FileExtension != '.idb' AND
                  System.FileExtension != '.tlog' AND
                  System.FileExtension != '.sdf' AND
                  System.FileExtension != '.ipch' AND
                  System.FileExtension != '.lastbuildstate' AND
                  System.FileExtension != '.user'";

        static string GetFilter(Task task)
        {
            if (!string.IsNullOrEmpty(task.FilterCache))
            {
                return task.FilterCache;
            }

            NavigateToAllOptions options = task.Provider.Options;
            string fileExtentionFilter = string.Empty;
            if (!string.IsNullOrWhiteSpace(options.AllowedFileExtensions))
            {
                string[] allowedExtensions = options.AllowedFileExtensions.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string extension in allowedExtensions)
                {
                    string trimmedExtension = extension.Trim();
                    if (trimmedExtension.StartsWith(".") && !trimmedExtension.Contains("'"))
                    {
                        fileExtentionFilter += string.Format(" OR System.FileExtension = '{0}'", trimmedExtension);
                    }
                }

                if (!string.IsNullOrWhiteSpace(fileExtentionFilter))
                {
                    fileExtentionFilter = fileExtentionFilter.Substring(4, fileExtentionFilter.Length - 4);
                }
            }

            if (string.IsNullOrWhiteSpace(fileExtentionFilter))
            {
                fileExtentionFilter = defaultFileExtensionFilter;
            }

            string solutionPathFilter = string.Empty;
            if (options.RestrictToSolutionFolder)
            {
                var dte = (EnvDTE.DTE)ServiceProvider.Current.GetService(typeof(EnvDTE.DTE));
                if (dte.Solution != null && File.Exists(dte.Solution.FullName))
                {
                    string solutionDirectory = Path.GetDirectoryName(dte.Solution.FullName);
                    solutionPathFilter = string.Format("SCOPE = 'file:{0}'", solutionDirectory.Replace('\\', '/'));
                    if (dte.Solution.Projects != null)
                    {
                        List<string> searchDirectories = new List<string> { solutionDirectory };
                        foreach (EnvDTE.Project project in dte.Solution.Projects)
                        {
                            if (!File.Exists(project.FullName))
                                continue;

                            string projectDirectory = Path.GetDirectoryName(project.FullName);
                            if (!Directory.Exists(projectDirectory))
                                continue;
                            if (searchDirectories.Any(d => projectDirectory.ToLower().StartsWith(d.ToLower())))
                                continue;
                            solutionPathFilter += string.Format(" OR SCOPE='file:{0}'", projectDirectory.Replace('\\', '/'));
                            searchDirectories.Add(projectDirectory);
                        }
                    }
                }
            }

            string filePathFilter = string.Empty;
            if (!string.IsNullOrWhiteSpace(options.RestrictToTheseFolders))
            {
                string[] allowedFolders = options.RestrictToTheseFolders.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string folder in allowedFolders)
                {
                    string directory = folder.Replace('\\', '/').Trim().TrimEnd('/');
                    if (Directory.Exists(directory))
                    {
                        filePathFilter += string.Format(" OR SCOPE='file:{0}'", directory);
                    }
                }

                if (!string.IsNullOrWhiteSpace(filePathFilter))
                {
                    filePathFilter = filePathFilter.Substring(4, filePathFilter.Length - 4);
                }
            }

            if (string.IsNullOrWhiteSpace(filePathFilter))
            {
                filePathFilter = defaultPathFilter;
            }

            filePathFilter = Environment.ExpandEnvironmentVariables(filePathFilter);
            if (!string.IsNullOrEmpty(solutionPathFilter))
            {
                solutionPathFilter = solutionPathFilter.Replace('\\', '/');
                if (filePathFilter == defaultPathFilter)
                {
                    task.FilterCache = string.Format("({0}) AND ({1})", fileExtentionFilter, solutionPathFilter);
                }
                else
                {
                    task.FilterCache = string.Format("({0}) AND ({1}) AND ({2})", filePathFilter, fileExtentionFilter, solutionPathFilter);
                }
            }
            else
            {
                task.FilterCache = string.Format("({0}) AND ({1})", filePathFilter, fileExtentionFilter);
            }

            return task.FilterCache;
        }

        public static void SearchFileNamePrefix(Task task)
        {
            if (!task.FileNamePrefixDone)
            {
                string escapedSearch = string.Empty;
                foreach (char c in task.SearchValue)
                {
                    if (c == '%' || c == '_' || c == '[' || c == ']')
                    {
                        escapedSearch += string.Format("[{0}]", c);
                    }
                    else
                    {
                        escapedSearch += c;
                    }
                }

                string query = string.Format(
                                @"SELECT TOP {2} System.ItemPathDisplay FROM SystemIndex WHERE ({0}) AND System.FileName LIKE '{1}%'",
                                GetFilter(task),
                                escapedSearch,
                                task.MaxCount);

                task.FileNamePrefixDone = !SearchInternal(task, query);
            }
        }

        public static void SearchFileNameSubstring(Task task)
        {
            if (!task.FileNameSubstringDone)
            {
                string query = string.Format(
                                @"SELECT TOP {2} System.ItemPathDisplay FROM SystemIndex WHERE ({0}) AND CONTAINS(System.FileName, '""{1}*""')",
                                GetFilter(task),
                                task.SearchValue,
                                task.MaxCount);

                task.FileNameSubstringDone = !SearchInternal(task, query);
            }
        }

        public static void SearchFileContentsRegular(Task task)
        {
            int count = task.Results.Count;
            if (!task.FileContentDone)
            {
                string query = string.Format(
                                @"SELECT TOP {2} System.ItemPathDisplay FROM SystemIndex WHERE ({0}) AND CONTAINS('""{1}""')",
                                GetFilter(task),
                                task.SearchValue,
                                task.MaxCount);
                
                task.FileContentDone = !SearchInternal(task, query);
            }

            if (task.IsCancelled)
                return;

            if (task.Results.Count == count)
            {
                if (!task.FileContentPrefixDone)
                {
                    string query = string.Format(
                            @"SELECT TOP {2} System.ItemPathDisplay FROM SystemIndex WHERE ({0}) AND CONTAINS('""{1}*""')",
                            GetFilter(task),
                            task.SearchValue,
                            task.MaxCount);

                    task.FileContentPrefixDone = !SearchInternal(task, query);
                }
            }
            else
            {
                task.FileContentPrefixDone = true;
            }
        }

        public static void SearchFileContentsFreeText(Task task)
        {
            if (!task.FreeTextDone)
            {
                string query = string.Format(
                                @"SELECT TOP {2} System.ItemPathDisplay FROM SystemIndex WHERE ({0}) AND FREETEXT('{1}')",
                                GetFilter(task),
                                task.SearchValue,
                                task.MaxCount);

                task.FreeTextDone = !SearchInternal(task, query);
            }
        }

        public static void Search(Task task)
        {
            int count = task.Results.Count;
            SearchFileNamePrefix(task);
            if (task.IsCancelled)
                return;

            SearchFileNameSubstring(task);
            if (task.IsCancelled)
                return;

            SearchFileContentsRegular(task);
            if (task.IsCancelled)
                return;

            if (task.Results.Count == count)
                SearchFileContentsFreeText(task);
            else
                task.FreeTextDone = true;

            if (task.IsCancelled)
                return;
                
            if (!task.FileContentDone || 
                !task.FileNamePrefixDone ||
                !task.FileNameSubstringDone ||
                !task.FreeTextDone ||
                !task.FileContentPrefixDone)
            {
                var moreItem = new NavigateToItem(
                    task.SortString, More, "", "", task, MatchKind.None, new NavigateToItemDisplayFactory());
                task.SortString = StringIncrement(task.SortString);
                task.Callback.AddItem(moreItem);
            }

            task.Callback.Done();
        }

        public static bool SearchInternal(Task task, string query)
        {
            var displayFactory = new NavigateToItemDisplayFactory();
            int count = 0;
            using (OleDbCommand command = new OleDbCommand(query, task.Connection))
            {
                command.CommandTimeout = 10;
                using (var reader = command.ExecuteReader())
                {
                    if (task.IsCancelled)
                        return false;

                    while (reader.Read())
                    {
                        if (task.IsCancelled)
                            return false;

                        string name = reader.GetString(0);
                        if (!task.Results.Contains(name) && File.Exists(name) && count < task.MaxCount - 1)
                        {
                            task.Callback.AddItem(
                                new NavigateToItem(task.SortString, name, "", "", task, MatchKind.None, displayFactory));
                            task.SortString = StringIncrement(task.SortString);
                            task.Results.Add(name);
                        }

                        ++count;
                    }
                }
            }

            return count == task.MaxCount;
        }

        static string StringIncrement(string s)
        {
            if (s[s.Length - 1] == 'z')
            {
                return s + "a";
            }
            else
            {
                return s.Substring(0, s.Length - 1) + (char)(s[s.Length - 1] + 1);
            }
        }
    }
}